﻿#region
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

#endregion

namespace MidnightPeach.EdmxTransformer
{
    public class EdmxHelper
    {
        private static string _csdl = "";
        private static string _msl = "";
        private static string _designer = "";

        private static void TransformDiagram(string edmxPath)
        {
            XDocument document = XDocument.Load(edmxPath);

            var elements = document.Descendants(XName.Get("EntityTypeShape", _designer));

            foreach (var item in elements)
                item.Attribute("EntityType").ReplaceEntity();

            using (XmlTextWriter writer = new XmlTextWriter(edmxPath, Encoding.Default))
                //            using (XmlTextWriter writer = new XmlTextWriter(@"D:\Practices\ConsoleApplication2\Target\EdmxTest.edmx.diagram", Encoding.Default))
            {
                document.WriteTo(writer);
            }
        }

        private static void FindNamespaces(string edmxPath)
        {
            var _namespaces = new[,]
                              {
                                  {
                                      "http://schemas.microsoft.com/ado/2008/09/edm",
                                      "http://schemas.microsoft.com/ado/2008/09/mapping/cs",
                                      "http://schemas.microsoft.com/ado/2008/10/edmx"
                                  },
                                  {
                                      "http://schemas.microsoft.com/ado/2009/11/edm",
                                      "http://schemas.microsoft.com/ado/2009/11/mapping/cs",
                                      "http://schemas.microsoft.com/ado/2008/10/edmx"
                                  }
                              };


            var content = File.ReadAllText(edmxPath);

            for (int i = 0; i < _namespaces.Length; i++)
            {
                if (content.Contains(_namespaces[i, 0]))
                {
                    _csdl = _namespaces[i, 0];
                    _msl = _namespaces[i, 1];
                    _designer = _namespaces[i, 2];
                    return;
                }
            }

            throw new NotImplementedException("Unsupported EDMX version.");
        }

        public static void Transform(string edmxPath, EdmxSchema schema)
        {
            FindNamespaces(edmxPath);

            XDocument document = XDocument.Load(edmxPath);

            XElement csdl = document.Descendants(XName.Get("Schema", _csdl)).First();
            XElement msl = document.Descendants(XName.Get("Mapping", _msl)).First();

            EdmxSchemaExtension.Schema = schema;
            EdmxSchemaExtension.Mapper = PrepareMapper(msl);

            TransformCsdl(csdl);
            TransformMsl(msl);

            using (XmlTextWriter writer = new XmlTextWriter(edmxPath, Encoding.Default))
                //            using (XmlTextWriter writer = new XmlTextWriter(@"D:\Practices\ConsoleApplication2\Target\EdmxTest.edmx", Encoding.Default))
            {
                document.WriteTo(writer);
            }

            TransformDiagram(edmxPath + ".diagram");
        }

        private static void TransformCsdl(XElement csdl)
        {
            foreach (var element in csdl.Elements(XName.Get("EntityContainer", _csdl)))
            {
                foreach (var entitySet in element.Elements(XName.Get("EntitySet", _csdl)))
                {
                    entitySet.Attribute("Name").ReplaceEntity(true);
                    entitySet.Attribute("EntityType").ReplaceEntity();
                }

                foreach (var associationSet in element.Elements(XName.Get("AssociationSet", _csdl)))
                    foreach (var end in associationSet.Elements(XName.Get("End", _csdl)))
                        end.Attribute("EntitySet").ReplaceEntity(true);
            }

            foreach (var entityType in csdl.Elements(XName.Get("EntityType", _csdl)))
            {
                string entityTo = entityType.Attribute("Name").Value;

                entityType.Attribute("Name").ReplaceEntity();

                foreach (var property in entityType.Elements(XName.Get("Property", _csdl)))
                    property.Attribute("Name").ReplaceProperty(entityTo);

                foreach (var key in entityType.Elements(XName.Get("Key", _csdl)))
                    foreach (var property in key.Elements(XName.Get("PropertyRef", _csdl)))
                        property.Attribute("Name").ReplaceProperty(entityTo);

                foreach (var navigationProperty in entityType.Elements(XName.Get("NavigationProperty", _csdl)))
                {
                    var relationship = navigationProperty.Attribute("Relationship").Value.GetLastDottedPart();
                    var fromRole = navigationProperty.Attribute("FromRole").Value;
                    var toRole = navigationProperty.Attribute("ToRole").Value;
                    navigationProperty.Attribute("Name").ReplaceNavigation(relationship, fromRole, toRole);
                }
            }

            foreach (var association in csdl.Elements(XName.Get("Association", _csdl)))
            {
                var entityToList = new List<string>();

                foreach (var end in association.Elements(XName.Get("End", _csdl)))
                {
                    entityToList.Add(end.Attribute("Type").Value.GetLastDottedPart());

                    end.Attribute("Type").ReplaceEntity();
                }

                var referentialConstraint = association.Element(XName.Get("ReferentialConstraint", _csdl));

                ReplaceConstraints(referentialConstraint, "Principal", entityToList[0]);
                ReplaceConstraints(referentialConstraint, "Dependent", entityToList[1]);
            }
        }

        private static void TransformMsl(XElement msl)
        {
            foreach (var entitySetMapping in msl.Element(XName.Get("EntityContainerMapping", _msl)).Elements(XName.Get("EntitySetMapping", _msl)))
            {
                foreach (var entityTypeMapping in entitySetMapping.Elements(XName.Get("EntityTypeMapping", _msl)))
                {
                    var entityTo = entityTypeMapping.Attribute("TypeName").Value.GetLastDottedPart();

                    foreach (var mappingFragment in entityTypeMapping.Elements(XName.Get("MappingFragment", _msl)))
                    {
                        foreach (var scalarProperty in mappingFragment.Elements(XName.Get("ScalarProperty", _msl)))
                            scalarProperty.Attribute("Name").ReplaceProperty(entityTo);

                        entityTypeMapping.Attribute("TypeName").ReplaceEntity();

                        entitySetMapping.Attribute("Name").ReplaceEntity(true);
                    }
                }
            }
        }

        private static void ReplaceConstraints(XElement referentialConstraint, string elementName, string entityTo)
        {
            var element = referentialConstraint.Element(XName.Get(elementName, _csdl));

            foreach (var propertyRef in element.Elements(XName.Get("PropertyRef", _csdl)))
                propertyRef.Attribute("Name").ReplaceProperty(entityTo);
        }

        #region helper methods
        private static Mapper PrepareMapper(XElement msl)
        {
            Mapper mapper = new Mapper();

            foreach (var entitySetMapping in msl.Element(XName.Get("EntityContainerMapping", _msl)).Elements(XName.Get("EntitySetMapping", _msl)))
            {
                var entitySet = entitySetMapping.Attribute("Name").Value;

                foreach (var entityTypeMapping in entitySetMapping.Elements(XName.Get("EntityTypeMapping", _msl)))
                {
                    var entityTo = entityTypeMapping.Attribute("TypeName").Value.GetLastDottedPart();

                    foreach (var mappingFragment in entityTypeMapping.Elements(XName.Get("MappingFragment", _msl)))
                    {
                        var entityFrom = mappingFragment.Attribute("StoreEntitySet").Value;

                        mapper.Add(entityTo, entityFrom);
                        mapper.Add(entitySet, entityFrom, true);

                        foreach (var scalarProperty in mappingFragment.Elements(XName.Get("ScalarProperty", _msl)))
                            mapper.Add(entityTo, scalarProperty.Attribute("Name").Value, scalarProperty.Attribute("ColumnName").Value);
                    }
                }
            }

            return mapper;
        }
        #endregion

        #region Extracting
        public static EdmxSchema ExtractSchema(string edmxFilePath)
        {
            FindNamespaces(edmxFilePath);

            EdmxSchema schema = new EdmxSchema();

            XDocument xdoc = XDocument.Load(edmxFilePath);

            XElement msl = xdoc.Descendants(XName.Get("Mapping", _msl)).First();
            XElement csdl = xdoc.Descendants(XName.Get("Schema", _csdl)).First();

            ExtractMsl(schema, msl);
            ExtractCsdl(schema, csdl);

            return schema;
        }

        private static void ExtractCsdl(EdmxSchema schema, XElement csdl)
        {
            var associations = ExtractAssociations(csdl);

            foreach (var entityType in csdl.Elements(XName.Get("EntityType", _csdl)))
            {
                string entityTypeName = entityType.Attribute("Name").Value;

                foreach (var navigationProperty in entityType.Elements(XName.Get("NavigationProperty", _csdl)))
                {
                    var entityRow = schema.Entity.FirstOrDefault(x => x.To == entityTypeName);
                    if (entityRow == null)
                        continue;

                    var navigationName = navigationProperty.Attribute("Name").Value;
                    var relationship = navigationProperty.Attribute("Relationship").Value.GetLastDottedPart();
                    var fromRole = navigationProperty.Attribute("FromRole").Value;
                    var toRole = navigationProperty.Attribute("ToRole").Value;

                    bool isMultiple = associations.Find(x => x.Name == relationship && x.Role == toRole).IsMultiple;

                    schema.Navigation.AddNavigationRow(entityRow, navigationName, navigationName, relationship, fromRole, toRole, isMultiple);
                }
            }
        }

        private static List<Association> ExtractAssociations(XElement csdl)
        {
            var associations = new List<Association>();

            foreach (var association in csdl.Elements(XName.Get("Association", _csdl)))
            {
                string name = association.Attribute("Name").Value;

                foreach (var end in association.Elements(XName.Get("End", _csdl)))
                {
                    string role = end.Attribute("Role").Value;
                    var isMultiple = end.Attribute("Multiplicity").Value == "*";
                    associations.Add(new Association(name, role, isMultiple));
                }
            }
            return associations;
        }

        private static void ExtractMsl(EdmxSchema schema, XElement msl)
        {
            foreach (var entitySetMapping in msl.Element(XName.Get("EntityContainerMapping", _msl)).Elements(XName.Get("EntitySetMapping", _msl)))
            {
                string set = entitySetMapping.Attribute("Name").Value;

                foreach (var entityTypeMapping in entitySetMapping.Elements(XName.Get("EntityTypeMapping", _msl)))
                {
                    string to = entityTypeMapping.Attribute("TypeName").Value.GetLastDottedPart();

                    foreach (var mappingFragment in entityTypeMapping.Elements(XName.Get("MappingFragment", _msl)))
                    {
                        var from = mappingFragment.Attribute("StoreEntitySet").Value;

                        EdmxSchema.EntityRow entityRow = schema.Entity.AddEntityRow(from, to, set);

                        foreach (var scalarProperty in mappingFragment.Elements(XName.Get("ScalarProperty", _msl)))
                        {
                            string propertyFrom = scalarProperty.Attribute("ColumnName").Value;
                            string propertyTo = scalarProperty.Attribute("Name").Value;

                            schema.Property.AddPropertyRow(entityRow, propertyFrom, propertyTo);
                        }
                    }
                }
            }
        }
        #endregion

        #region Applying Rules
        public static void ApplyRules(EdmxSchema schema)
        {
            ApplyPropertyRule(schema);
            ApplyNavigationRule(schema);
            ApplyPrefixRule(schema);
            ApplyPostfixRule(schema);
        }

        private static void ApplyPropertyRule(EdmxSchema schema)
        {
            foreach (var property in schema.Property)
            {
                if (property.From != property.To)
                    continue;

                var rule = schema.PropertyRule.FindByFrom(property.From);

                if (rule == null)
                    continue;

                property.To = rule.To;
            }
        }

        private static void ApplyNavigationRule(EdmxSchema schema)
        {
            foreach (var navigation in schema.Navigation)
            {
                if (navigation.From != navigation.To)
                    continue;

                var entity = schema.Entity.FindByFrom(navigation.From);
                if (entity == null)
                    continue;

                navigation.To = navigation.IsMultiple ? entity.Set : entity.To;
            }
        }

        private static void ApplyPrefixRule(EdmxSchema schema)
        {
            Func<string, string, bool> predicate = (row, rule) => row.StartsWith(rule);

            foreach (var prefix in schema.PrefixRule)
            {
                if (prefix.IsToNull())
                    prefix.To = string.Empty;

                ApplyRuleToTable(schema.Entity, prefix, predicate);
                ApplyRuleToTable(schema.Property, prefix, predicate);
                ApplyRuleToTable(schema.Navigation, prefix, predicate);
            }
        }

        private static void ApplyPostfixRule(EdmxSchema schema)
        {
            Func<string, string, bool> predicate = (row, rule) => row.EndsWith(rule);

            foreach (var postfix in schema.PostfixRule)
            {
                if (postfix.IsToNull())
                    postfix.To = string.Empty;

                ApplyRuleToTable(schema.Entity, postfix, predicate);
                ApplyRuleToTable(schema.Property, postfix, predicate);
                ApplyRuleToTable(schema.Navigation, postfix, predicate);
            }
        }

        private static void ApplyRuleToTable(DataTable table, IRule rule, Func<string, string, bool> predicate)
        {
            foreach (IRow row in table.Rows)
                if (row.From == row.To && predicate(row.From, rule.From))
                    row.To = row.From.Replace(rule.From, rule.To);
        }
        #endregion
    }
}